package spacewar2.simulator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import spacewar2.actions.SpacewarAction;
import spacewar2.configs.SpacewarConfig;
import spacewar2.objects.Asteroid;
import spacewar2.objects.Base;
import spacewar2.objects.Beacon;
import spacewar2.objects.Bullet;
import spacewar2.objects.Ship;
import spacewar2.objects.SpacewarActionableObject;
import spacewar2.objects.SpacewarObject;
import spacewar2.objects.SpacewarWeapon;
import spacewar2.powerups.SpacewarPowerup;
import spacewar2.shadows.Shadow;
import spacewar2.utilities.Movement;
import spacewar2.utilities.Position;
import spacewar2.utilities.Vector2D;

/**
 * Physics engine for the spacewar simulator.  
 * The Toroidal part refers to wrapping around the edges of the simulation.
 * 
 * @author amy
 */
public class Toroidal2DPhysics {
	/**
	 * Height and width of the simulation 
	 */
	int height, width;

	float halfHeight, halfWidth;

	/**
	 * All objects in the space
	 */
	ArrayList<SpacewarObject> allObjects;

	/**
	 * The list of beacons
	 */
	ArrayList<Beacon> beacons;
	
	/**
	 * The list of asteroids
	 */
	ArrayList<Asteroid> asteroids;
	
	/**
	 * The list of bases
	 */
	ArrayList<Base> bases;

	/**
	 * The list of ships
	 */
	ArrayList<Ship> ships;
	
	/**
	 * List of all weapons currently in play
	 */
	ArrayList<SpacewarWeapon> weapons;
	
	/**
	 * A hashmap of objects by their ID
	 */
	HashMap <UUID, SpacewarObject> objectsById;
	
	/**
	 * The timestep used for simulation of physics
	 */
	double timeStep;
	
	/**
	 * The current timestep iteration
	 */
	int currentTimeStep;
	
	/**
	 * Used to add temporary things to the GUI (like weapons)
	 */
	ArrayList<Shadow> newShadows, oldShadows;

	/**
	 * Maximum velocities (to keep things from going nuts)
	 */
	public static double MAX_TRANSLATIONAL_VELOCITY = 200;
	public static double MAX_ANGULAR_VELOCITY = Math.PI;
	public static double ENERGY_PENALTY = 0.5;

	/**
	 * Handles collisions between spacewar objects
	 */
	CollisionHandler collisionHandler;

	/**
	 * Constructor for the regular game
	 * @param simConfig
	 */
	public Toroidal2DPhysics(SpacewarConfig simConfig) { 
		height = simConfig.getHeight();
		width = simConfig.getWidth();
		halfHeight = height / 2.0f;
		halfWidth = width / 2.0f;
		allObjects = new ArrayList<SpacewarObject>();
		timeStep = simConfig.getSimulationTimeStep();
		collisionHandler = new CollisionHandler();
		beacons = new ArrayList<Beacon>();
		asteroids = new ArrayList<Asteroid>();
		bases = new ArrayList<Base>();
		ships = new ArrayList<Ship>();
		newShadows = new ArrayList<Shadow>();
		oldShadows = new ArrayList<Shadow>();
		weapons = new ArrayList<SpacewarWeapon>();
		objectsById = new HashMap<UUID, SpacewarObject>();
	}

	/**
	 * Constructor for unit tests
	 * @param height
	 * @param width
	 * @param timeStep
	 */
	public Toroidal2DPhysics(int height, int width, double timeStep) {
		super();
		this.height = height;
		this.width = width;
		this.timeStep = timeStep;
		halfHeight = height / 2.0f;
		halfWidth = width / 2.0f;
		allObjects = new ArrayList<SpacewarObject>();
		collisionHandler = new CollisionHandler();
		beacons = new ArrayList<Beacon>();
		asteroids = new ArrayList<Asteroid>();
		bases = new ArrayList<Base>();
		ships = new ArrayList<Ship>();
		newShadows = new ArrayList<Shadow>();
		oldShadows = new ArrayList<Shadow>();
		weapons = new ArrayList<SpacewarWeapon>();
		objectsById = new HashMap<UUID, SpacewarObject>();
	}

	/**
	 * Make a shallow copy of the space with just the settings copied over and new array lists created.
	 *
	 * This is used by the cloning (and should not be called otherwise)
	 * 
	 * @param other
	 */
	private Toroidal2DPhysics(Toroidal2DPhysics other) {
		super();
		this.height = other.height;
		this.width = other.width;
		this.timeStep = other.timeStep;
		this.currentTimeStep = other.currentTimeStep;
		halfHeight = height / 2.0f;
		halfWidth = width / 2.0f;
		allObjects = new ArrayList<SpacewarObject>();
		collisionHandler = new CollisionHandler();
		beacons = new ArrayList<Beacon>();
		asteroids = new ArrayList<Asteroid>();
		bases = new ArrayList<Base>();
		ships = new ArrayList<Ship>();
		newShadows = new ArrayList<Shadow>();
		oldShadows = new ArrayList<Shadow>();
		weapons = new ArrayList<SpacewarWeapon>();
		objectsById = new HashMap<UUID, SpacewarObject>();
	}
	
	

	/**
	 * Add an object to the physics simulation
	 * @param obj
	 */
	public void addObject(SpacewarObject obj) {
		allObjects.add(obj);

		if (obj.getClass() == Beacon.class) {
			beacons.add((Beacon) obj);
		}
		
		if (obj.getClass() == Asteroid.class) {
			asteroids.add((Asteroid) obj);
		}
		
		if (obj.getClass() == Base.class) {
			bases.add((Base) obj);
		}
		
		if (obj.getClass() == Ship.class) {
			ships.add((Ship) obj);
		}
		
		if (obj instanceof SpacewarWeapon) {
			weapons.add((SpacewarWeapon)obj);
		}
		
		objectsById.put(obj.getId(), obj);
	}


	/**
	 * Delete an object from the physics simulation
	 * @param obj
	 */
	public void removeObject(SpacewarObject obj) {
		allObjects.remove(obj);

		if (obj.getClass() == Beacon.class) {
			beacons.remove((Beacon) obj);
		}
		
		if (obj.getClass() == Asteroid.class) {
			asteroids.remove((Asteroid) obj);
		}
		
		if (obj.getClass() == Base.class) {
			bases.remove((Base) obj);
		}
		
		if (obj.getClass() == Ship.class) {
			ships.remove((Ship) obj);
		}
		
		if (obj instanceof SpacewarWeapon) {
			weapons.remove((SpacewarWeapon)obj);
		}
		
		objectsById.remove(obj.getId());
	}

	/**
	 *  return object by its ID
	 * @param id
	 * @return
	 */
	public SpacewarObject getObjectById(UUID id) {
		return objectsById.get(id);
	}

	/**
	 * Return the list of asteroids
	 * @return
	 */
	public ArrayList<Asteroid> getAsteroids() {
		return asteroids;
	}
	
	/**
	 * Return the list of beacons
	 * 
	 * @return
	 */
	public ArrayList<Beacon> getBeacons() {
		return beacons;
	}
	
	/**
	 * Return the list of bases
	 * 
	 * @return
	 */
	public ArrayList<Base> getBases() {
		return bases;
	}

	/**
	 * Return the list of ships
	 * @return
	 */
	public ArrayList<Ship> getShips() {
		return ships;
	}
	
	/**
	 * Return a list of weapons currently in play
	 * @return
	 */
	public ArrayList<SpacewarWeapon> getWeapons() {
		return weapons;
	}

	/**
	 * Return the Environment height
	 * @return
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * Return the Environment width
	 * @return
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * Return the timestep duration
	 * @return
	 */
	public double getTimestep() {
		return timeStep;
	}
	
	/**
	 * Return the timestep duration
	 * @return
	 */
	public int getCurrentTimestep() {
		return currentTimeStep;
	}
	
	/**
	 * Returns a new random free location in space
	 * 
	 * @param rand Random number generator
	 * @param radius the radius around the new location that must be free
	 * @return
	 */
	public Position getRandomFreeLocation(Random rand, int radius) {
		Position randLocation = new Position(rand.nextFloat() * width, rand.nextFloat() * height);

		while (!isLocationFree(randLocation, radius)) {
			randLocation = new Position(rand.nextFloat() * width, rand.nextFloat() * height);
		}

		return randLocation;
	}

	/**
	 * Returns a new random free location in space
	 * 
	 * @param rand Random number generator
	 * @param freeRadius the radius around the object that must be free
	 * @return
	 */
	public Position getRandomFreeLocationInRegion(Random rand, int freeRadius, 
			int centerX, int centerY, double maxDistance) {
		Position centerPosition = new Position(centerX, centerY);
		double newX = ((2 * rand.nextDouble()) - 1) * maxDistance + centerX;
		double newY = ((2 * rand.nextDouble()) - 1) * maxDistance + centerY;
		Position randLocation = new Position(newX, newY);
		toroidalWrap(randLocation);

		while (!isLocationFree(randLocation, freeRadius) || findShortestDistance(centerPosition, randLocation) > maxDistance) {
			newX = ((2 * rand.nextDouble()) - 1) * maxDistance + centerX;
			newY = ((2 * rand.nextDouble()) - 1) * maxDistance + centerY;
			randLocation = new Position(newX, newY);
			toroidalWrap(randLocation);
		}

		return randLocation;
	}



	/**
	 * Is the specified location free (within the specified radius)?
	 * 
	 * @param location
	 * @param radius
	 * @return true if the location is free and false otherwise
	 */
	public boolean isLocationFree(Position location, int radius) {
		for (SpacewarObject object : allObjects) {
			if (findShortestDistanceVector(object.getPosition(), location).getMagnitude() <= (radius + object.getRadius())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Public interface to find the shortest toroidal distance from one location to another.
	 * Returns a vector pointing from location1 to location2.  Use a getMagnitude() call
	 * on the vector to get the distance.
	 * 
	 * @param location1
	 * @param location2
	 * @return
	 */
	public Vector2D findShortestDistanceVector(Position location1, Position location2) {
		return findShortestDistanceVector(location1, location2, width, height, halfWidth, halfHeight);
	}

	/**
	 * Public interface to find the shortest toroidal distance from one location to another.
	 * Returns a double (the distance).  Use findShortestDistanceVector to get the vector
	 * telling you which way to move along this path.  Useful if you just
	 * care about distance.
	 * 
	 * @param location1
	 * @param location2
	 * @return
	 */
	public double findShortestDistance(Position location1, Position location2) {
		Vector2D shortDist = findShortestDistanceVector(location1, location2, width, height, halfWidth, halfHeight);
		return shortDist.getMagnitude();
	}


	/**
	 * Finds the shortest distance in toroidal space. Returns a vector
	 * pointing from the start to the target location and getMagnitude can be used
	 * to find the distance and the angle.
	 * 
	 * @param location1
	 * @param location2
	 * @param width
	 * @param height
	 * @param halfWidth
	 * @param halfHeight
	 * @return
	 */
	private Vector2D findShortestDistanceVector(Position location1,
			Position location2, float width, float height,
			float halfWidth, float halfHeight) {
		double x = location2.getX() - location1.getX();
		double y = location2.getY() - location1.getY();
		if (x > halfWidth) {
			if (y > halfHeight) {
				return new Vector2D(x - width, y - height);
			} else if (y < -halfHeight) {
				return new Vector2D(x - width, y + height);
			} else {
				return new Vector2D(x - width, y);
			}
		} else if (x < -halfWidth) {
			if (y > halfHeight) {
				return new Vector2D(x + width, y - height);
			} else if (y < -halfHeight) {
				return new Vector2D(x + width, y + height);
			} else {
				return new Vector2D(x + width, y);
			}
		} else if (y > halfHeight) {
			return new Vector2D(x, y - height);
		} else if (y < -halfHeight) {
			return new Vector2D(x, y + height);
		} else {
			return new Vector2D(x, y);
		}
	}

	/**
	 * Move all moveable objects and handle power ups.
	 */
	public void advanceTime(int currentTimeStep, Map<UUID, SpacewarPowerup> powerups) {
		
		this.currentTimeStep = currentTimeStep;
		
		// heal any base injuries
		for (Base base : bases) {
			base.updateEnergy(Base.ENERGY_HEALING_INCREMENT);
		}
		
		// detect collisions across all objects
		detectCollisions();

		// get the power ups and create any objects (weapons) as necessary
		for (UUID key : powerups.keySet()) {
			SpacewarObject swobject = getObjectById(key);
			// if the object is not alive or it is not actionable, then ignore this
			if (!swobject.isAlive() || (!(swobject instanceof SpacewarActionableObject))) {
				continue;
			}

			// otherwise, handle the power up
			handlePowerup(swobject, powerups.get(key));
		}

		// now move all objects that are moveable (which may include weapons)
		for (SpacewarObject object : allObjects) {
			// skip non-moveable objects or dead object
			if (!object.isMoveable() || !object.isAlive()) {
				continue;
			}
			
			Position currentPosition = object.getPosition();

			// is it a ship that can be controlled?
			if (object.isControllable()) {

				Ship ship = (Ship) object;
				SpacewarAction action = ship.getCurrentAction();

				Movement actionMovement = action.getMovement(this, ship);

				Position newPostion = applyMovement(currentPosition, actionMovement, timeStep);
				ship.setPosition(newPostion);
				
				// spent ship energy proportional to its acceleration (old formula used velocity)
				//double penalty = ENERGY_PENALTY * -Math.abs(ship.getPosition().getTotalTranslationalVelocity());
				double accel1 = Math.abs(actionMovement.getAngularAccleration());
				double accel2 = actionMovement.getTranslationalAcceleration().getMagnitude();
				double penalty = ENERGY_PENALTY * Math.max(accel1, accel2);
				ship.updateEnergy(-penalty);
				
			} else {
				// move all other types of objects
				Position newPosition = moveOneTimestep(currentPosition);
				object.setPosition(newPosition);
			}
		}
		
		// go through and see if any bases have died
		ArrayList<Base> basesClone = (ArrayList<Base>) bases.clone();
		for (Base base : basesClone) {
			if (!base.isHomeBase() && base.getHealingEnergy() <= 0) {
				base.setAlive(false);
				addOldShadow(base.getShadow());
				removeObject(base);
				base.getTeam().removeBase(base);
			}
		}
		
		
	}

	/**
	 * Handle power ups for the specified object
	 * TODO: Note this only handles Bullets right now
	 * @param swobject
	 * @param spacewarPowerup
	 */
	private void handlePowerup(SpacewarObject swobject,	SpacewarPowerup spacewarPowerup) {
		if (spacewarPowerup instanceof SpacewarWeapon) {
			SpacewarWeapon weapon = (SpacewarWeapon) spacewarPowerup;
			Ship ship = (Ship) getObjectById(weapon.getFiringShip().getId());
			if (weapon != null && weapon.isValidWeapon(ship)) { 
				addObject(weapon);
				ship.updateEnergy(weapon.getCostToUse());
				addNewShadow(weapon.getShadow());
				ship.incrementBulletCount();
				weapon.setFiringShip(ship);
			}
		}
		
	}

	/**
	 * Add a shadow to the list of shadows for the GUI
	 * @param shadow
	 */
	private void addNewShadow(Shadow shadow) {
		newShadows.add(shadow);
	}
	
	/**
	 * Add a shadow to the list of shadows for the GUI to be removed
	 * @param shadow
	 */
	private void addOldShadow(Shadow shadow) {
		oldShadows.add(shadow);
	}

	
	/**
	 * Reset the new shadow list (because they have been added to the gui)
	 */
	public void resetNewShadows() {
		newShadows.clear();
	}

	/**
	 * Reset the old shadow list (because they have been removed from the gui)
	 */
	public void resetOldShadows() {
		oldShadows.clear();
	}

	/**
	 * Get the new shadows for the GUI
	 * @return
	 */
	public ArrayList<Shadow> getNewShadows() {
		return newShadows;
	}

	/**
	 * Get any shadows to remove from the GUI
	 * @return
	 */
	public ArrayList<Shadow> getOldShadows() {
		return oldShadows;
	}

	/**
	 * Advances one time step using the set velocities
	 * @param currentPosition
	 * @return
	 */
	private Position moveOneTimestep(Position position) {
		double angularVelocity = position.getAngularVelocity();
		double orientation =  position.getOrientation() + (angularVelocity * timeStep);

		// make sure orientation wraps correctly (-pi to pi)
		if (orientation > Math.PI) {
			orientation -= (2 * Math.PI);
		} else if (orientation < -Math.PI) {
			orientation += (2 * Math.PI);
		}

		// new x,y coordinates
		double newX = position.getX() + (position.getTranslationalVelocityX() * timeStep);
		double newY = position.getY() + (position.getTranslationalVelocityY() * timeStep);

		Position newPosition = new Position(newX, newY, orientation);
		newPosition.setAngularVelocity(angularVelocity);
		newPosition.setTranslationalVelocity(position.getTranslationalVelocity());
		toroidalWrap(newPosition);
		return newPosition;
	}

	/**
	 * Step through all the objects and ensure they are not colliding.  If they are,
	 * call the collision handler for those objects.  Sometimes you bounce (asteroids)
	 * and sometimes you pick the object up (beacons), etc.
	 */
	private void detectCollisions() {
		// loop through all pairs of objects and see if they are colliding
		for (int objIndex1 = 0; objIndex1 < allObjects.size(); objIndex1++) {
			SpacewarObject object1 = allObjects.get(objIndex1);
			if (!object1.isAlive()) {
				continue;
			}
			
			
			for (int objIndex2 = objIndex1 + 1; objIndex2 < allObjects.size(); objIndex2++) {
				SpacewarObject object2 = allObjects.get(objIndex2);
				if (!object2.isAlive()) {
					continue;
				}

				
				double distance = findShortestDistance(object1.getPosition(), object2.getPosition());

				if (distance < (object1.getRadius() + object2.getRadius())) {
					collisionHandler.collide(object1, object2, this);
				}
			}
		}
	}

	/**
	 * Takes an acceleration and a simulation time step and moves the object
	 * 
	 * @param actionMovement
	 * @param timeStep
	 * @return
	 */
	public Position applyMovement(Position position, Movement movement, double timeStep) {
		double translationalAccelX = movement.getTranslationalAcceleration().getXValue();
		double translationalAccelY = movement.getTranslationalAcceleration().getYValue();
		double angularAccel = movement.getAngularAccleration();

		// velocity is acceleration times time
		double translationalVelocityX = position.getTranslationalVelocityX() + (translationalAccelX * timeStep);
		double translationalVelocityY = position.getTranslationalVelocityY() + (translationalAccelY * timeStep);
		double angularVelocity = position.getAngularVelocity() + (angularAccel * timeStep);

		// ensure the max/mins are respected
		translationalVelocityX = checkTranslationalVelocity(translationalVelocityX);
		translationalVelocityY = checkTranslationalVelocity(translationalVelocityY);
		angularVelocity = checkAngularVelocity(angularVelocity);

		Position newPosition = new Position(position.getX(), position.getY(), position.getOrientation());
		newPosition.setTranslationalVelocity(new Vector2D(translationalVelocityX, translationalVelocityY));
		newPosition.setAngularVelocity(angularVelocity);

		return moveOneTimestep(newPosition);
	}

	/**
	 * Ensure the angular velocity doesn't exceed the max
	 * @param angularVelocity
	 * @return
	 */
	private double checkAngularVelocity(double angularVelocity) {
		if (angularVelocity > MAX_ANGULAR_VELOCITY) {
			return MAX_ANGULAR_VELOCITY;
		} else if (angularVelocity < -MAX_ANGULAR_VELOCITY) {
			return -MAX_ANGULAR_VELOCITY;
		} else {
			return angularVelocity;
		}
	}

	/**
	 * Ensure the translational velocity doesn't get too large
	 * @param translationalVelocity
	 * @return
	 */
	private double checkTranslationalVelocity(double translationalVelocity) {
		if (translationalVelocity > MAX_TRANSLATIONAL_VELOCITY) {
			return MAX_TRANSLATIONAL_VELOCITY;
		} else if (translationalVelocity < -MAX_TRANSLATIONAL_VELOCITY) {
			return -MAX_TRANSLATIONAL_VELOCITY;
		} else {
			return translationalVelocity;
		}

	}

	/**
	 * Torridial wrap based on the height/width of the enviroment
	 *  
	 * @param position
	 */
	void toroidalWrap(Position position) {
		while (position.getX() < 0) {
			position.setX(position.getX() + width);
		}

		while (position.getY() < 0) {
			position.setY(position.getY() + height);
		}

		position.setX(position.getX() % width);
		position.setY(position.getY() % height);
	}

	/**
	 * Respawns any dead objects in new random locations.  Ships
	 * have a delay before they can respawn.
	 */
	public void respawnDeadObjects(Random random) {
		for (SpacewarObject object : allObjects) {
			if (!object.isAlive() && object.canRespawn()) {
				Position newPostion = getRandomFreeLocation(random, object.getRadius() * 2);
				object.setPosition(newPostion);
				object.setAlive(true);
			}
		}

	}

	/**
	 * Clones all the objects in space (used for security so the teams can't manipulate other ships)
	 * 
	 * @return
	 */
	public Toroidal2DPhysics deepClone() {
		Toroidal2DPhysics newSpace = new Toroidal2DPhysics(this);

		for (SpacewarObject swObject : allObjects) {
			SpacewarObject newObject = swObject.deepClone();
			
			newSpace.addObject(newObject);
		}
		
		return newSpace;
	}

	/**
	 * Loop through all weapons and remove any dead ones
	 */
	public void cleanupDeadWeapons() {
		ArrayList<SpacewarObject> deadObjects = new ArrayList<SpacewarObject>();
		for (SpacewarObject object : allObjects) {
			if (object.getClass() == Bullet.class && !object.isAlive()) {
				addOldShadow(object.getShadow());
				deadObjects.add(object);
			}
		}
		
		for (SpacewarObject deadObject : deadObjects) {
			removeObject(deadObject);
		}
		
	}

	

}
